/* 
    WhaleSpotter
    Copyright (C) 2007  Bob Mottram
    fuzzgun@gmail.com

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace whalespotter
{
    public class whaleDatabase
    {
        const String DATABASE_VERSION = "1.00";

        const int MAX_INDIVIDUALS = 30000;
        const int MAX_INDEX_MISSES = 4;
        const int MAX_TEXTURE_DIFFERENCE = 70;
        int RECOGNITION_THRESHOLD = 1000;
        const int MIN_RECOGNITION_THRESHOLD = 1000;
        const int MAX_RECOGNITION_THRESHOLD = 100000000;

        // weightings used for score calculation
        // some experimentation may be needed to find good values for these
        // If enough data is available an optimisation method should
        // be used to automatically discover suitable values
        public float weight_key_features = 1.0f;
        public float weight_linear_features = 2.0f;
        public float weight_edge_pattern = 1.0f;


        public int no_of_individuals;
        Byte[,] main_index;
        int[] Texture;
        whale[] individual;
        ArrayList filenames = new ArrayList();

        ArrayList candidates = new ArrayList();
        ArrayList candidate_score = new ArrayList();


        public whaleDatabase()
        {
            no_of_individuals = 0;
            main_index = new Byte[MAX_INDIVIDUALS, 20 * 2];
            individual = new whale[MAX_INDIVIDUALS];
            Texture = new int[MAX_INDIVIDUALS];
        }

        /// <summary>
        /// set the recognition threshold
        /// </summary>
        /// <param name="percentage"></param>
        public void setRecognitionThreshold(int percentage)
        {
            if (percentage < 0) percentage = 0;
            if (percentage > 100) percentage = 100;
            RECOGNITION_THRESHOLD = MIN_RECOGNITION_THRESHOLD + (percentage * (MAX_RECOGNITION_THRESHOLD - MIN_RECOGNITION_THRESHOLD) / 100);
        }


        /// <summary>
        /// returns the matching score between this individual and the given one
        /// </summary>
        /// <param name="Index"></param>
        /// <param name="max_misses"></param>
        /// <returns></returns>
        private int matchingScore(Byte[] Index1, int Index2, int max_misses)
        {
            int misses = 0;
            int score = 0;
            int n = 0;
            int x = 0;
            while ((x < 20) && (misses < max_misses))
            {
                int y = 0;
                while ((y < 2) && (misses < max_misses))
                {
                    if (Index1[n] > 0)
                    {
                        if (main_index[Index2, n] > 0)
                        {
                            if (Index1[n] == main_index[Index2, n])
                                score++;
                            else
                                misses++;
                        }
                    }
                    n++;
                    y++;
                }
                x++;
            }
            if (misses >= max_misses) score = -1;
            return (score);
        }

        /// <summary>
        /// find the given individual in the database
        /// </summary>
        /// <param name="w"></param>
        /// <returns></returns>
        public ArrayList Find(whale w, ArrayList search_indexes, bool compareEdgePattern)
        {
            ArrayList results;

            String filename = ((sighting)w.Sightings[0]).ImageFilename;
            int idx = filenames.IndexOf(filename);
            if (idx == -1)
            {
                // return the top scoring individuals
                results = getCandidates(5, w.Index, w.Texture, w.keyFeatures, w.linearFeaturesAll, w.edge_pattern, search_indexes, compareEdgePattern);
            }
            else
            {
                // this image has already been stored in the database
                results = new ArrayList();
                results.Add(individual[idx]);
                search_indexes.Add(idx);
            }
            return (results);
        }

        /// <summary>
        /// generate a list of candidate matches
        /// </summary>
        /// <param name="index"></param>
        /// <param name="texture"></param>
        private ArrayList getCandidates(int max_results, Byte[] index, int texture, 
                                   ArrayList key_features, ArrayList linear_features_all, bool[] edge_pattern,
                                   ArrayList search_indexes, bool compareEdgePattern)
        {
            ArrayList initial_indexes = new ArrayList();
            ArrayList initial_candidates = new ArrayList();
            ArrayList initial_scores = new ArrayList();
            int max_score = 0;
            for (int i = 0; i < no_of_individuals; i++)
            {
                if (individual[i].useForIdentification)
                {
                    int texture_diff = Math.Abs(texture - Texture[i]);
                    if (texture_diff < MAX_TEXTURE_DIFFERENCE)
                    {
                        int score = matchingScore(index, i, MAX_INDEX_MISSES);
                        int texture_score = (1 + (((MAX_TEXTURE_DIFFERENCE - texture_diff) * 50) / MAX_TEXTURE_DIFFERENCE));
                        score = (score * 3) + texture_score;
                        //score = score * texture_score;

                        if (score > 0)
                        {
                            int sc = 9999999;
                            int j = 0;
                            while ((score < sc) && (j < initial_candidates.Count))
                            {
                                sc = (int)initial_scores[j];
                                j++;
                            }
                            if (j < initial_candidates.Count)
                            {
                                if (j > 0) j--;
                                initial_candidates.Insert(j, individual[i]);
                                initial_indexes.Insert(j, i);
                                initial_scores.Insert(j, score);
                            }
                            else
                            {
                                initial_candidates.Add(individual[i]);
                                initial_indexes.Add(i);
                                initial_scores.Add(score);
                            }
                        }

                    }
                }
            }

            ArrayList secondary_candidates = new ArrayList();
            ArrayList secondary_indexes = new ArrayList();
            ArrayList secondary_scores = new ArrayList();
            candidate_score.Clear();
            int max_evaluations = 1000;
            if (initial_candidates.Count < max_evaluations) max_evaluations = initial_candidates.Count;
            max_score = RECOGNITION_THRESHOLD;
            for (int i = 0; i < max_evaluations; i++)
            {
                whale w = (whale)initial_candidates[i];
                int score = (int)(w.keyFeatureScore(key_features, false) * weight_key_features);
                score += (int)(w.keyFeatureScore(linear_features_all, true) * weight_linear_features);

                if (compareEdgePattern)
                    score += (int)(w.edgePatternScore(edge_pattern)* weight_edge_pattern);

                score *= (int)initial_scores[i];

                if (score > RECOGNITION_THRESHOLD)
                {
                    int sc = 9999999;
                    int j = 0;
                    while ((score < sc) && (j < secondary_candidates.Count))
                    {
                        sc = (int)secondary_scores[j];
                        j++;
                    }
                    if (j < secondary_candidates.Count)
                    {
                        if (j > 0) j--;
                        secondary_candidates.Insert(j, w);
                        secondary_indexes.Insert(j, (int)initial_indexes[i]);
                        secondary_scores.Insert(j, score);
                    }
                    else
                    {
                        secondary_candidates.Add(w);
                        secondary_indexes.Add((int)initial_indexes[i]);
                        secondary_scores.Add(score);
                    }
                }

                /*
                if (score > max_score)
                {
                    secondary_candidates.Insert(0, w);
                    secondary_indexes.Insert(0, (int)initial_indexes[i]);
                    candidate_score.Insert(0, score);
                    max_score = score;
                }
                 */
            }

            ArrayList candidates = new ArrayList();
            int max = max_results;
            if (secondary_candidates.Count < max) max = secondary_candidates.Count;
            for (int i = 0; i < max; i++)
            {
                candidates.Add((whale)secondary_candidates[i]);
                search_indexes.Add((int)secondary_indexes[i]);
            }

            return (candidates);
        }

        public int Add(whale new_sighting)
        {
            int status = 0;

            if (no_of_individuals < MAX_INDIVIDUALS)
            {
                String filename = ((sighting)new_sighting.Sightings[0]).ImageFilename;
                int idx = filenames.IndexOf(filename);
                int idx2 = no_of_individuals;
                if (idx > -1) idx2 = idx;

                individual[idx2] = new_sighting;
                Texture[idx2] = new_sighting.Texture;
                for (int i = 0; i < 20 * 2; i++)
                    main_index[idx2, i] = new_sighting.Index[i];

                status = 2;
                if (idx == -1)
                {
                    no_of_individuals++;
                    filenames.Add(filename);
                    status = 1;
                }
                
            }
            else status = -2;
            return (status);
        }

        /// <summary>
        /// save the database
        /// </summary>
        /// <param name="filename"></param>
        public void Save(String filename)
        {
            FileStream fp = new FileStream(filename, FileMode.Create);
            BinaryWriter binfile = new BinaryWriter(fp);

            binfile.Write(DATABASE_VERSION);

            binfile.Write(no_of_individuals);
            for (int i = 0; i < no_of_individuals; i++)
            {
                individual[i].Save(binfile);
            }

            binfile.Close();
            fp.Close();
        }

        /// <summary>
        /// export the data to a CSV file for spreadsheet analysis
        /// </summary>
        /// <param name="filename"></param>
        public void ExportToCSV(String filename)
        {
            StreamWriter oWrite = null;
            bool allowWrite = true;

            try
            {
                oWrite = File.CreateText(filename);
            }
            catch
            {
                allowWrite = false;
            }

            if (allowWrite)
            {
                oWrite.Write(whale.exportCSVTitle());
                for (int i = 0; i < no_of_individuals; i++)
                {
                    whale w = (whale)individual[i];
                    String csv_line = w.exportCSV();
                    oWrite.Write(csv_line);
                }
                oWrite.Close();
            }
        }

        /// <summary>
        /// returns a list containing all sighting for the given species
        /// </summary>
        /// <param name="species"></param>
        /// <returns></returns>
        public ArrayList getSightingsBySpecies(String species, String individualName)
        {
            ArrayList results = new ArrayList();

            for (int i = 0; i < no_of_individuals; i++)
            {
                whale w = (whale)individual[i];
                if ((individualName == "All") || (w.Name == individualName))
                {
                    if ((w.Species == species) || (species == "All"))
                    {
                        for (int j = 0; j < w.Sightings.Count; j++)
                            results.Add((sighting)w.Sightings[j]);
                    }
                }
            }
            return (results);
        }

        /// <summary>
        /// returns a list of individual names for the given species
        /// </summary>
        /// <param name="species"></param>
        /// <returns></returns>
        public ArrayList getNamesBySpecies(String species)
        {
            ArrayList results = new ArrayList();

            for (int i = 0; i < no_of_individuals; i++)
            {
                whale w = (whale)individual[i];
                if ((w.Species == species) || (species == "All"))
                {
                    if (!results.Contains(w.Name))
                        results.Add(w.Name);
                }
            }
            return (results);
        }

        /// <summary>
        /// returns the number of unique individuals in the database
        /// </summary>
        /// <returns></returns>
        public int getNumberOfIndividuals()
        {
            ArrayList results = getNamesBySpecies("All");
            return (results.Count);
        }

        /// <summary>
        /// returns the number of species in the database
        /// </summary>
        /// <returns></returns>
        public int getNumberOfSpecies()
        {
            ArrayList results = new ArrayList();

            for (int i = 0; i < no_of_individuals; i++)
            {
                whale w = (whale)individual[i];
                if (!results.Contains(w.Species))
                    results.Add(w.Species);
            }
            return (results.Count);
        }

        /// <summary>
        /// return the total number of sightings in the database
        /// </summary>
        /// <returns></returns>
        public int getNumberOfSightings()
        {
            int no_of_sightings = 0;
            for (int i = 0; i < no_of_individuals; i++)
            {
                whale w = (whale)individual[i];
                no_of_sightings += w.Sightings.Count;
            }
            return (no_of_sightings);
        }

        /// <summary>
        /// load the database
        /// </summary>
        /// <param name="filename"></param>
        public void Load(String filename)
        {
            if (File.Exists(filename))
            {
                filenames = new ArrayList();
                FileStream fp = new FileStream(filename, FileMode.Open);
                BinaryReader binfile = new BinaryReader(fp);

                String version = binfile.ReadString();

                no_of_individuals = binfile.ReadInt32();
                for (int i = 0; i < no_of_individuals; i++)
                {
                    whale w = new whale();
                    w.Load(binfile);
                    individual[i] = w;

                    String imageFilename = ((sighting)w.Sightings[0]).ImageFilename;
                    Texture[i] = w.Texture;
                    for (int j = 0; j < 20 * 2; j++)
                        main_index[i, j] = w.Index[j];
                    filenames.Add(imageFilename);
                }

                binfile.Close();
                fp.Close();
            }
        }
    }
}
